package com.hup.minicontroller.client.service;

import com.hup.minicontroller.client.model.AppConfig;
import com.hup.minicontroller.client.model.AppState;
import com.hup.minicontroller.client.model.LanBroadcastServer;
import com.hup.minicontroller.client.mvp.MainMvp.IMainView;
import com.hup.minicontroller.common.constant.BroadcastConstant;
import com.hup.utils.commons.start.SpringContext;
import com.hup.utils.commons.thread.component.PrefixAbleThreadFactory;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.TreeSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 局域网UPD广播接收端;
 * 说明在安卓项目.server.LanBroadcastSenderService
 *
 * @author hugan
 * @date 2021/5/10
 */
@Log4j2
@Component
public class LanBroadcastReceiverService {
    private static final long SERVER_EXPIRE_TIME = 10000;

    /**
     * 接收广播的独立线程;接收广播时是阻塞线程的,所以不能使用公用的Handler
     */
    private final ExecutorService executor;
    private final TreeSet<LanBroadcastServer> lanBroadcastServers;
    private Receiver receiver;

    private boolean isStart;
    /**
     * 上次清理过期搜索结果的时间
     */
    private long lastClearExpireServer;

    public LanBroadcastReceiverService() {
        executor = Executors.newSingleThreadExecutor(new PrefixAbleThreadFactory("LanBroadcastReceiver"));
        lanBroadcastServers = new TreeSet<>();
        lastClearExpireServer = System.currentTimeMillis();
        try {
            receiver = new Receiver();
        } catch (Exception e) {
            log.error("UDP广播初始化异常", e);
        }
    }

    public void switchService(boolean isStart) {
        log.info("switchService={}", isStart);
        if (receiver == null || this.isStart == isStart) return;
        this.isStart = isStart;
        if (isStart) {
            log.info("开始接收广播");
            executor.execute(receiver);
        } else {
            lanBroadcastServers.clear();
        }
    }

    public void doTerminate() {
        log.info("doTerminate: ");
        if (receiver != null) {
            receiver.doTerminate();
            lanBroadcastServers.clear();
            receiver = null;
        }
        executor.shutdown();
    }

    private void onReceived(String serverName, String hostAddress) {
        log.debug("serverName={}, address={}", serverName, hostAddress);
        LanBroadcastServer newServer = new LanBroadcastServer(serverName, hostAddress, System.currentTimeMillis());
        lanBroadcastServers.remove(newServer);//移除旧item,注意此处重写过equal
        lanBroadcastServers.add(newServer);//TreeSet.add时,按照address排序,避免界面上的item跳来跳去
        SpringContext.executeIfExist(IMainView.class, i -> i.onScannedBroadcastServer(lanBroadcastServers));
    }

    private void clearExpireService() {
        long cur = System.currentTimeMillis();
        if (cur - lastClearExpireServer < SERVER_EXPIRE_TIME) return;
        log.debug("");
        Iterator<LanBroadcastServer> it = lanBroadcastServers.iterator();
        boolean changed = false;
        while (it.hasNext()) {
            LanBroadcastServer server = it.next();
            if (cur - server.getReceiveTime() > SERVER_EXPIRE_TIME) {
                it.remove();
                changed = true;
            }
        }
        if (changed)
            SpringContext.executeIfExist(IMainView.class, i -> i.onScannedBroadcastServer(lanBroadcastServers));
        lastClearExpireServer = cur;
    }

    /**
     * <pre>
     * UDP广播接收端,正常代码流程:
     * .初始化MulticastSocket:端口号,接收超时
     * .初始化DatagramPacket,用于接收数据
     * .MulticastSocket加入广播组,注意这是加入一次就可以了
     * .循环开始接受数据
     *
     * 安卓joinGroup问题:
     * .无网时(没连wifi,也没开移动网络),joinGroup会异常
     * .joinGroup后,断网重连,会长时间都timeout,但后续可能会接收成功
     * .上述两个问题,只在安卓端出现,电脑版没有此问题,所以本类采用[每次接收数据时都重新joinGroup]的方案,避免网络切换时出现长时间都没在"接收状态"的情况
     */
    private class Receiver implements Runnable {
        private final AppState appState;

        private final InetAddress broadcastAddress;
        private MulticastSocket multicastSocketReceiver;
        private final byte[] buff;
        private final DatagramPacket packet;

        public Receiver() throws IOException {
            appState = SpringContext.getBean(AppState.class);
            AppConfig appConfig = appState.getAppConfig();
            broadcastAddress = InetAddress.getByName(appConfig.getBroadcastIp());
            multicastSocketReceiver = new MulticastSocket(appConfig.getLanBroadcastPort());
            multicastSocketReceiver.setSoTimeout(BroadcastConstant.CLIENT_RECEIVE_TIMEOUT);
            buff = new byte[1024];
            packet = new DatagramPacket(buff, buff.length);
            multicastSocketReceiver.joinGroup(broadcastAddress);
        }

        @Override
        public void run() {
            if (!isStart || appState.getConnectedType() != null) {
                log.info("停止接收广播");
                isStart = false;
                return;
            }
            clearExpireService();
            try {
                packet.setData(buff);//每次receive前都要重置packet
                multicastSocketReceiver.receive(packet);
                int readLength = packet.getLength();
                if (readLength == 0 || readLength < BroadcastConstant.MAX_MSG_LENGTH) {
                    byte[] data = packet.getData();
                    String msg = new String(data, 0, readLength, StandardCharsets.UTF_8);
                    //log.info("收到广播={}", msg);
                    if (msg.startsWith(BroadcastConstant.MSG_HEAD)) {
                        msg = msg.substring(BroadcastConstant.MSG_HEAD.length());
                        String hostAddress = packet.getAddress().getHostAddress();
                        onReceived(msg, hostAddress);
                    }
                }
            } catch (IOException e) {
                /*
                一般是SocketTimeoutException,原因:
                .服务端无上线
                .已联网情况下,断网了
                 */
                log.debug("广播接收错误: " + e);
            }
            if (!isStart) {
                log.info("停止接收广播");
            } else {
                /*
                .消息接收成功:马上继续接收(可能有多个服务端的消息)
                .消息接收失败:也马上继续接收,因为一般是超时异常,已经等得了n秒了
                 */
                executor.execute(this);
            }
        }

        public void doTerminate() {
            isStart = false;
            if (!multicastSocketReceiver.isClosed()) {
                try {
                    multicastSocketReceiver.leaveGroup(broadcastAddress);
                    multicastSocketReceiver.close();
                } catch (IOException e) {
                    log.info("终止异常?", e);
                }
            }
        }


    }

}
